package edu.mtu.citizenscience.cs.sdk.uploader;

import java.io.IOException;
import java.util.List;

import com.androidquery.AQuery;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import edu.mtu.citizenscience.cs.sdk.ResponseHandler;
import edu.mtu.citizenscience.cs.sdk.ServerRequest;
import edu.mtu.citizenscience.cs.sdk.json.Request;
import edu.mtu.citizenscience.cs.sdk.login.CitsciLogin;
import edu.mtu.citizenscience.cs.sdk.uploader.json.DB;
import edu.mtu.citizenscience.cs.sdk.uploader.json.exception.InvalidRelationship;
import edu.mtu.citizenscience.cs.sdk.uploader.json.exception.InvalidTableName;
import edu.mtu.citizenscience.cs.sdk.uploader.model.Row;
import edu.mtu.citizenscience.cs.sdk.uploader.upload.parts.Upload;
import edu.mtu.citizenscience.cs.utils.Services;
import edu.mtu.citizenscience.cs.utils.services.StopHandler;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
/**
 * @author Corbin Uselton <corbinu@cwuselto@mtu.edu>
 * @copyright (c) 2012 MichiganTech <http://www.mtu.edu>
 * @since 1.0
 * 
 * Uploader service which runs in the background behind an
 * app and upload reports to the CitSci server
 */
public class CitsciUploaderService extends Service {
	// the app db to upload data from
	private DB db;
	// android-query library
	private AQuery aq;
	
	// reference var for passing the main table to the service
	public static final String MAIN_TABLE = "MAIN_TABLE";
	// reference var for passing progress from the service
	public static final String PROGRESS = "PROGRESS";
	// reference var for passing report num from the service
	public static final String REPORT_NUM = "REPORT_NUM";
	// reference var for passing file num from the service
	public static final String FILE_NUM = "FILE_NUM";
	// reference var for passing progress max from the service
	public static final String PROGRESS_MAX = "PROGRESS_MAX";
	// reference var for passing progress update from the service
	public static final String PROGRESS_UPDATE = "PROGRESS_UPDATE";
	// reference var for passing error from the service
	public static final String ERROR = "ERROR";
	// reference var for passing error message from the service
	public static final String ERROR_MESSAGE = "ERROR_MESSAGE";
	// reference var for passing upload complete from the service
	public static final String UPLOAD_COMPLETE = "UPLOAD_COMPLETE";

	// holds the last progress
	private CitsciUploaderProgress lastProgress;
	// holds the last progress max
	private int lastProgressMax = -1;
	// holds the last progress update
	private int lastProgressUpdate = -1;
	// holds the last report num
	private int lastReportNum = -1;
	// holds the last file num
	private int lastFileNum = -1;
	// holds the last error
	private CitsciUploaderError lastError = CitsciUploaderError.NONE;
	// holds the last error message
	private String lastErrorMessage;
	// holds if the last upload was complete
	private boolean lastUploadComplete = false;
	// holds if the service is currently uploading or not
	private boolean isUploading = false;
	
	// stops the service after a set amount of time
	private StopHandler stopHandler = new StopHandler(this);
	// Used to receive messages from the managing CitsicUploader object
	final Messenger inMessenger = new Messenger(new IncomingHandler());
	// Used to send message to the managing CitsciUploader object
	private Messenger outMessenger;
	
	/**
	 * @since 1.0
	 * 
	 * Handles messages from the managing CitsciUploader object
	 */
	@SuppressLint("HandlerLeak")
	class IncomingHandler extends Handler 
	{
		/**
		 * Called when a message from the managing CitsciUploader object is received
		 * 
		 * @param   message - Message - The message from the service
		 * @return	void
		 */
		@Override
	    public void handleMessage(Message message) 
	    {
			// get the main table for upload from the message data
			String mainTable = message.getData().getString(MAIN_TABLE);
			// check to see if got the main table
      		if (mainTable != null)
      		{
      			Log.d("Uploader", "Starting upload of table " + mainTable);
      			try
    			{
      				// start the upload
          			startUpload(mainTable);
    			} 
    			catch (JsonParseException e) 
    			{
    				e.printStackTrace();
    			} 
    			catch (JsonMappingException e) 
    			{
    				e.printStackTrace();
    			} 
    			catch (IOException e) 
    			{
    				e.printStackTrace();
    			} 
    			catch (InvalidTableName e) 
    			{
    				e.printStackTrace();
    			} 
    			catch (InvalidRelationship e) 
    			{
    				e.printStackTrace();
    			}
      			
      			// send message to the to the activity that the upload has begun
      			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, new Bundle());
    		}
      		else
      		{
      			// send message to the to the activity that the upload has not begun
      			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_CANCELED, new Bundle());
      		}
		}
	}
	
	/**
	 * Create the Uploader Service
	 * 
	 * @param   void
	 * @return	void
	 */
	public CitsciUploaderService()
	{
		super();
		
		// create android-query instance
		this.aq = new AQuery(this);
	}
	
	/**
	 * Get the services AQuery instance
	 * 
	 * @param   void
	 * @return	AQuery - the services android query instance
	 */
	public AQuery getAq()
	{
		return this.aq;
	}

	/**
	 * Called when the Uploader Service is bound
	 * 
	 * @param   intent - Intent - The Intent that was used to bind to this service
	 * @return	IBinder
	 */
	@Override
	public IBinder onBind(Intent intent) 
	{
		Log.d("Uploader", "Upload service bound");
		// get the intent extras
		Bundle extras = intent.getExtras();
		// check if got extras from the intent
		if (extras != null) 
		{
			// set the out messenger passed in the data bundle
    		this.outMessenger = (Messenger) extras.get(Services.MESSENGER);
		}
		
		// check if the uploader has a current progress to pass on to the bound activity
		if (this.lastProgress != null)
		{
			// send the current upload progress, report num and file num
			this.sendProgress(this.lastProgress, this.lastReportNum, this.lastFileNum);
			// clear the progress, report num and file num
			this.lastProgress = null;
			this.lastReportNum = -1;
			this.lastFileNum = -1;
		}
		
		// check if the uploader has a current progress max to pass on to the bound activity
		if (this.lastProgressMax > -1)
		{
			// send the current progress max
			this.sendProgressMax(lastProgressMax);
			// clear the progress max
			this.lastProgressMax = -1;
		}
		
		// check if the uploader has a current progress update to pass on to the bound activity
		if (this.lastProgressUpdate > -1)
		{
			// send the current progress update
			this.sendProgressUpdate(lastProgressUpdate);
			// clear the progress update
			this.lastProgressUpdate = -1;
		}
		
		// check if the uploader has a current error to pass on to the bound activity
		if (this.lastError != CitsciUploaderError.NONE)
		{
			// send the current error
			this.sendError(lastError);
			// clear the error
			this.lastError = CitsciUploaderError.NONE;
		}
		
		// check if the uploader has a current error message to pass on to the bound activity
		if (this.lastErrorMessage != null)
		{
			// send the current error message
			this.sendErrorMessage(lastErrorMessage);
			// clear the error message
			this.lastErrorMessage = null;
		}
		
		// check if the uploader has a completed an activity to pass on to the bound activity
		if (this.lastUploadComplete)
		{
			// send the upload complete
			this.sendUploadComplete();
			// clear the upload complete
			this.lastUploadComplete = false;
		}
		
		// Return messenger to this service to the managing CitsciUploader object
		return this.inMessenger.getBinder();
	}
	
	/**
	 * Called when the Upload Service is unbound
	 * 
	 * @param   intent - Intent - The Intent that was used to unbind to this service
	 * @return	boolean - return true so that onRebind will be called
	 */
	@Override
	public boolean onUnbind(Intent intent)
	{
		// start the stop timer to end the service in 5 min
		this.stopHandler.stopIn(5 * 60);
		
		return true;
	}
	
	/**
	 * Called when the Upload Service is rebound
	 * 
	 * @param   intent - Intent - The Intent that was used to rebind to this service
	 * @return	void
	 */
	@Override
	public void onRebind(Intent intent)
	{
		// service has been rebound so cancel the timer
		this.stopHandler.cancel();
	}
	
	/**
	 * Start the upload
	 * 
	 * @param   mainTable - String - the main table to upload
	 * @return	void
	 */
	public void startUpload(String mainTable) throws JsonParseException, JsonMappingException, IOException, InvalidTableName, InvalidRelationship
	{
		// check if the uploader is already upload
		if (this.isUploading)
		{
			// send error that the service is already uploading
			this.sendError(CitsciUploaderError.ALREADY_UPLOADING);
			
			return;
		}
		
		// set that the uploader is uploading
		this.isUploading = true;
		
		// send progress as processing
		this.sendProgress(CitsciUploaderProgress.PROCESSING, -1, -1);
		// load the DB in from the schema doc using Jackson JSON
		db = new ObjectMapper().readValue(this.aq.getContext().getResources().getAssets().open("json/db_schema.json"), DB.class);
		// get the rows from the db for the main table
		List<Row> rows = db.getRows(this.aq.getContext(), mainTable);
		
		// check that have any rows to upload
		if (rows.size() > 0)
		{
			// set the progress max to the number of rows
			this.sendProgressMax(rows.size());
			// check if have stored login
			if (new CitsciLogin(aq).getLogin().haveLogin())
			{
				// create a new upload and start the upload
				 new Upload(this, rows).startUpload();
			}
			else
			{
				// send the error that the user is not logged in
				this.sendError(CitsciUploaderError.NOT_LOGGED_IN);
			}
		}
		else
		{
			// send the error that there are no entries
			this.sendError(CitsciUploaderError.NO_ENTRIES);
		}
	}
	
	/**
	 * Send the progress max to the managing Citsci Uploder object
	 * 
	 * @param   max - int - the max progress possible
	 * @return	void
	 */
	public void sendProgressMax(int max)
	{
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
			// put the progress max in the data bundle
	  		bundle.putInt(PROGRESS_MAX, max);
	  		
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the max value to send to the activity when the service is rebound
			this.lastProgressMax = max;
		}
	}
	
	/**
	 * Send the progress update to the managing Citsci Uploder object
	 * 
	 * @param   update - int - the progress value
	 * @return	void
	 */
	public void sendProgressUpdate(int update)
	{
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
			// put the progress value in the data bundle
	  		bundle.putInt(PROGRESS_UPDATE, update);
	  		
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the progress value to send to the activity when the service is rebound
			this.lastProgressUpdate = update;
		}
	}
	
	/**
	 * Send the progress to the managing Citsci Uploder object
	 * 
	 * @param   progress - int - the progress state
	 * @param	reportNum - int - the report num uploading if any
	 * @param	fileNum - int - the file num uploading if any
	 * @return	void
	 */
	public void sendProgress(CitsciUploaderProgress progress, int reportNum, int fileNum)
	{
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
			// put the progress in the data bundle
	  		bundle.putString(PROGRESS, progress.toString());
	  		// put the report num in the data bundle
	  		bundle.putInt(REPORT_NUM, reportNum);
	  		// put the file num in the data bundle
	  		bundle.putInt(FILE_NUM, fileNum);
	  	
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the progress, report num and file num to send to the activity when the service is rebound
			this.lastProgress = progress;
			this.lastReportNum = reportNum;
			this.lastFileNum = fileNum;
		}
	}
	
	/**
	 * Send the error to the managing Citsci Uploder object
	 * 
	 * @param   error - CitsciUploaderError - the uploader error
	 * @return	void
	 */
	public void sendError(CitsciUploaderError error)
	{
		// mark that the uploader is no longer uploading
		this.isUploading = false;
		
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
	  		// put the error in the data bundle
	  		bundle.putString(ERROR, error.toString());
	  		
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the error to send to the activity when the service is rebound
			this.lastError = error;
		}
	}
	
	/**
	 * Send the error message to the managing Citsci Uploder object
	 * 
	 * @param   message - String - the error message
	 * @return	void
	 */
	public void sendErrorMessage(String message)
	{
		// mark that the uploader is no longer uploading
		this.isUploading = false;
		
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
	  		// put the error message in the data bundle
	  		bundle.putString(ERROR_MESSAGE, message);
	  		
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the error message to send to the activity when the service is rebound
			this.lastErrorMessage = message;
		}
	}
	
	/**
	 * Wrap up uploading
	 * 
	 * @param   rows - List<Row> - the rows uploaded
	 * @return	void
	 */
	public void uploadComplete(List<Row> rows)
	{
		// mark the rows as uploaded
		this.db.setUploaded(this.aq.getContext(), rows);
		
		// mark that the uploader is no longer uploading
		this.isUploading = false;
		
		// send that the upload is complete
		this.sendUploadComplete();
	}
	
	/**
	 * Send to the managing Citsci Uploader object that the upload 
	 * 
	 * @param   rows - List<Row> - the rows uploaded
	 * @return	void
	 */
	public void sendUploadComplete()
	{
		// check that the service is bound
		if (this.outMessenger != null)
		{
			// create data bundle to hold the message data
			Bundle bundle = new Bundle();
			// put the upload complete in the data bundle
	  		bundle.putBoolean(UPLOAD_COMPLETE, true);
	  		
	  		// send the message and ok status to the managing Citsci Uploader object
			Services.sendMessage("Uploader", outMessenger, Activity.RESULT_OK, bundle);
		}
		else
		{
			// save the upload is complete to send to the activity when the service is rebound
			this.lastUploadComplete = true;
		}
	}
	
	/**
	 * Send upload request to the Citsci Server
	 * 
	 * @param   request - Request<?> - the request for the api
	 * @param	handler - ResponseHandler<?> - the handler for the request
	 * @return	void
	 */
	public void doRequest(Request<?> request, ResponseHandler<?> handler)
	{	
		// do the upload request on the citsci api
		ServerRequest.doRequest(this.aq, "upload/" + ServerRequest.UPLOAD_TYPE, request, handler);
	}

}
